next up previous
Siguiente: Acerca de este documento ... Arriba: Introducción a la Programación Orientada a Objetos Anterior: Referencias

Subsecciones

A Soluciones a los Ejercicios

  Esta sección presenta ejemplos de solución a los ejercicios de las lecciones anteriores.

A.1 Una Revisión a las Técnicas de Programación

1.
Discusión del módulo Singly-Linked-List-2.
(a)
Definición de la Interface del módulo Integer-List
   MODULE Integer-List

   DECLARE TYPE int_list_handle_t;

   int_list_handle_t int_list_create();
   BOOL    int_list_append(int_list_handle_t this,
                           int data);
   INTEGER int_list_getFirst(int_list_handle_t this);
   INTEGER int_list_getNext(int_list_handle_t this);
   BOOL    int_list_isEmpty(int_list_handle_t this);
   
   END Integer-List;

Esta representación presenta problemas adicionales que son causados por no separar el recorrido, de la estructura de los datos. Como podrás recordar, para iterar sobre los elementos de la lista, hemos usado un bucle con la siguiente condición :

   WHILE data IS VALID DO

Data se inicializó con una llamada a list_getFirst(). El procedimieno de la lista integer int_list_getFirst() regresa un entero (integer), por consecuencia, no existe algo como un "integer no válido", lo cuál podríamos usar para un chequeo de terminación del bucle.

2.
Diferencias entre programación orientada a objetos y otras técnicas. En la programación orientada a objetos, los objetos intercambian mensajes entre sí. En otras técnicas de programación, los datos son intercambiados entre procedimientos bajo el control de un programa principal. Objetos del mismo tipo pero cada uno con su propio status pueden coexistir. Esto contrasta con el enfoque modular donde cada módulo tiene solamente un status global.

A.2 Tipos de Datos Abstracto

1.
TDA Integer.
(a)
Ambas operaciones add y sub pueden ser aplicadas para cualquier valor de N. Así, estas operaciones pueden ser aplicadas en cualquier momento: No hay restricción a su uso. Sin embargo, tú puedes describir ésto con una precondición que sea igual a verdadero.

(b)
Definimos aquí tres operaciones nuevas : mul, div y abs. Esta última debería regresar un valor absoluto del entero. Las operaciones se definen como sigue :

  mul(k)
  div(k)
  abs()

La operación mul no requiere ninguna precondición. Esto es similar a add y sub. La postcondición es por supuesto res = N*k. La siguiente operació div requiere que k no valga 0 (cero). Consecuentemente, definimos la siguiente precondición : k no igual a 0. La última operación : abs regreas el valor de N si N es positivo o vale 0 o, -N si N es negativo. Nuevamente, no importa que valor tiene N cuando se aplica esta operación. Aquí está su postcondición :

if N >= 0 then
abs = N
else
abs = -N
2.
TDA Fraction.
(a)
Una fracción simple consiste de numerador y denominador. Ambos son números enteros. Esto es similar al ejemplo de los números complejos presentados en la sección. Podríamos escoger al menos dos estructuras de datos para contener los valores : un arreglo o un registro.

(b)
Esquema de la interface. Recuerda que la interface es solamente el conjunto de operaciones visible al mundo exterior. Podríamos describir una interface para una fracción en una manera verbal. Consecuentemente, necesitamos las operaciones :
  • para obtener (get) el valor numerador/denominador,
  • para establecer (set) el valor numerador/denominador,
  • para sumar una fracción, regesando la suma,
  • para restar una fracción regresando la diferencia,
  • ...

(c)
Tenemos aquí algunos axiomas y precondiciones para cada fracción que valen también para el TDA :
  • El denominador no debe ser igual a 0 (cero), de otro modo, el valor de la fracción no es definido.
  • Si el numerador es igual a 0 (cero) el valor de la fracción es 0 para cualquier valor del denominador.
  • Todo número entero puede ser representado por una fracción cuyo numerador es el número y el denominador es 1.
  • 3.
    Los TDAs definen las propiedades de un conjunto de instancias. Proveen una vista abstracta de estas propiedades aportando un conjunto de operaciones que pueden ser aplicadas a dichas instancias. Es este conjunto de operaciones, la interface, el que define las propiedades de las instancias. El uso de un TDA está restringido por axiomas y precondiciones. Ambos definen las condiciones y las propiedades de un ambiente en el cuál instancias del TDA pueden ser usadas.

    4.
    Necesitamos declarar axiomas y definir precondiciones para asegurar el uso correcto de las instancias de los TDAs. Por ejemplo, si nosotros no declaramos que 0 sea un elemento neutral en la adición de enteros, podría haber un TDA Integer que hiciera algo extraño cuando se sumara 0 a N. Esto no es lo que se espera de un entero. Así, los axiomas y precondiciones proveen un medio para asegurarse que los TDAs "funcionen" tal como queremos que lo hagan.

    5.
    Descripción de relaciones.
    (a)
    Una instancia es una representación funcional de un TDA. Es por lo tanto un "ejemplo" de él. Cuando un TDA declara usar un "número entero con signo" en su estructura de datos, una instancia contiene de hecho un valor, digamos, "-5".
    (b)
    Los TDAs genéricos definen las mismas propiedades de su TDA correspondiente. Sin embargo, están dedicados a otro tipo en particular. Por ejemplo, el TDA List define propiedades de listas. Así, podríamos tener una operación append(elem) que agrega un nuevo elemento elem a la lista. No decimos que tipo elem es en realidad, solamente que será el último elemento de la lista después de esta operación. Si ahora usamos un TDA genérico List, el tipo de elemento es conocido : es provisto por el parámetro genérico.
    (c)
    Las instancias del mismo TDA genérico podrían ser vistos como "hermanos". Serían "primos" de instancias de otro TDA genérico si ambos TDAs genéricos comparten el mismo TDA.

    A.3 Conceptos de Orientación a Objetos

    1.
    Clase.
    (a)
    Una clase es la implementación real de un TDA. Por ejemplo, un TDA para integers pudieran incluir la operación set para establecer el valor de su instancia. Esta operación se implementa en forma diferente en lenguajes tales como C o Pascal. En C, el signo igual "=" define el conjunto de operaciones para integers, mientras que en Pascal se usa la cadena de caracteres " :=". Consecuentemente, las clases implementan operaciones al proveer métodos. En forma similar, la estructura de datos del TDA es implementado por atributos de la clase.

    (b)
    Clase Complex
      class Complex {
      attributes:
        Real real,
             imaginario
    
      methods:
        :=(Complex c)    /* Poner el valor a lo que vale c */
        Real realPart()
        Real imaginaryPart()
        Complex +(Complex c)
        Complex -(Complex c)
        Complex /(Complex c)
        Complex *(Complex c)
      }
    

    Escogemos los bien conocidos símbolos de operador "+" para la adición, "-" para la resta, "/" para la división y "*" para la multiplicación al implementar las operaciones correspondientes del TDA Complex. Así, objetos de la clase Complex pueden ser usados del siguiente modo:

      Complex c1, c2, c3
      c3 := c1 + c2
    

    Podrás notar, que podríamos escribir la instrucción de la adición como sigue :

      c3 := c1.+(c2)
    

    Podrías querer remplazar el "+" con "add" para aproximarse a una representación que ya hemos usado. Sin embargo, deberías poder entender que "+" no es otra cosa que un nombre diferente para "add".

    2.
    Objetos interactuantes.

    3.
    Perspectiva del objeto.

    4.
    Mensajes.
    (a)
    Los objetos son entidades autónomas que solamente proveen una bien definida interface. Quisiéramos hablar de objetos como si fueran entidades activas. Por ejemplo, los objetos "son responsables" de sí mismos, "ellos" podrían prohibir la invocación de un método, etc.. Esto distingue un objeto de un módulo, el cuál es pasivo. Por lo tanto, no hablamos de llamadas a porocedimientos. Hablamos de mensajes con los cuáles "le pedimos" a un objeto que invoque uno de sus métodos.

    (b)
    El internet provee varios objetos. Dos de los más conocidos son "cliente" y "servidor". Por ejemplo, tú usas un cliente (objeto) de FTP para acceder a datos almacenados en un servidor (objeto) de FTP. De ahí que, se podría ver ésto como si el cliente "mandara un mensaje" al servidor pidiéndole que le provea de datos almacenados ahí.

    (c)
    En el ambiente cliente/servidor, tenemos realmente dos entidades actuando en forma remota : los procesos de cliente y de servidor. Típicamente, estas dos entidades intercambian datos en forma de mensajes de Internet.

    A.4 Más Conceptos Orientados a Objetos

    1.
    Herencia.
    (a)
    Definición de la clase Rectángulo:
      class Rectangle inherits from Point {
      attributes:
        int _width,     // Base del rectángulo
            _height     // Altura del rectángulo
    
      methods:
        setWidth(int newWidth)
        getWidth()
        setHeight(int newHeight)
        getHeight()
      }
    

    En este ejemplo, definimos un rectángulo por su esquina superior izquierda (las coordenadas tal como se heredaron de Point) y sus dimensiones. Alternativamente, lo podríamos haber definido por su esquina superior izquierda y su esquina inferior derecha.

    Añadimos métodos de acceso para la base y la altura del rectángulo.

    (b)
    Objetos en 3a. dimensión. Una esfera se define por un centro en un espacio en 3a. dimensión, y por un radio. El centro es un punto en un espacio en 3a. dimensión, así, podemos definir la clase Sphere (esfera) como:
      class Sphere inherits from 3D-Point {
      attributes:
        int _radius;
    
      methods:
        setRadius(int newRadius)
        getRadius()
      }
    

    Esto es similar a la clase círculo para un espacio en 2a. dimensión. Ahora3D-Point es solamente un Point con una dimensión adicional:

      class 3D-Point inherits from Point {
      attributes:
        int _z;
    
      methods:
        setZ(int newZ);
        getZ();
      }
    

    Consecuentemente, 3D-Point y Point tienen una relación es-un(a).

    (c)
    Funcionalidad de move().

    move() como se definió en la sección le permite a los objetos de 3a. dimensión moverse a lo largo del eje-X, es decir, en una sola dimensión. Hace esto, al modificar solamente la parte de 2a. dimensión de los objetos de 3a. dimensión. Esta parte de 2a. dimensión está definida por la clase Point heredada directamente o indirectamente por los objetos de 3a. dimensión.

    (d)
    Gráfica de herencia (ver la Figura A.1).
     
    Figura A.1:  Gráfica de herencia de algunos objetos desplegables.
    \begin{figure}
 {\centerline{
\psfig {file=FIGS/solig.eps,width=9cm}
}}
 \end{figure}

    (e)
    Gráfica de herencia alternativa. En este ejemplo, la clase Sphere hereda de Circle y simplemente añade una tercera coordenada. Esto tiene la ventaja de que una esfera puede ser manejada como un círculo (por ejemplo, su radio puede ser fácilmente modificado por métodos/funciones que manejan círculos). Tiene la desventaja de que "distribuye" el manejador (el centro en el espacio de 3a. dimensión) sobre la jerarquía de herencia : va desde Point pasando sobre Circle a Sphere. De ahí que este manejador no esté accesible como un todo.

    2.
    Herencia múltiple. La gráfica de herencia en la Figura 5.9 obviamente introduce conflictos de nomenclatura por las propiedades de la clase A.

    Sin embargo, estas propiedades se identifican en forma única siguiendo la trayectoria hacia arriba desde D hasta A. Así, D puede cambiar las propiedades de A heredadas por B al seguir la trayectoria de herencia a través de B. En forma similar, D puede cambiar las propiedades de A heredadas por C al seguir la trayectoria de herencia a través de C. Consecuentemente, este conflicto de nomenclatura no necesariamente conlleva a error, mientras las trayectorias estén designadas.

    A.5 Más sobre C++

    1.
    Polimorfismo. Cuando se usa la "signature"
      void display(const DrawableObject obj);
    
    nótese primero, que en C++ los parámetros de funciones o métodos son pasados por valor. Consecuentemente, obj sería una copia del argumento de llamada a función realmente provisto. Esto significa que DrawableObject debe ser una clase de la cuál se pueden crear objetos. Este no es el caso, si DrawableObject es una clase abstracta (tal como sucede cuando print() se define como un método puro.)

    Si existe un método virtual print() que está definido por la clase DrawableObject, entonces (como obj es solamente una copia del argumento real) este método es invocado. Este no es el método definido por la clase del argumento real (¡debido a que ya no juega ningún rol significativo !)

    A.6 La Lista - Estudio de un Caso

    1.
    Operador de preincremento para iteradores. El operador de preincremento tal como se define en el ejercicio no checa la validez de _current. Como succ() pudiera poner su valor a NULL ésto pudiera causar acceso a este apuntador NULL y, de ahí, bloquear el programa. Una posible solución pudiera ser definir el operador como :
    T &operator ++() {
      succ();
      return(_current ? _current->data() : (T) 0);
    }
    
    Sin embargo, esto no funciona como estamos asumiendo ahora algo sobre T. Debe ser posible convertirlo a un tipo de valor "NULL".

    2.
    Escritura del método remove. No damos la solución codificada. En lugar de eso, proporcionamos el algoritmo. El método remove() debe iterar sobre la lista hasta que alcance el elemento con el ítem de datos solicitado. Hecho lo cuál, elimina el elemento y regresa 1. Si la lista está vacía, o, si el ítem de datos no puede ser encontrado, regresa 0 (cero).

    Durante la iteración, remove() debe comparar el ítem de datos provisto sucesivamente con aquéllos en la lista. Consecuentemente, podría existir una comparación como :

      if (data == current->data()) {
        // se encontró el ítem
      }
    

    Usamos aquí el operador de ecuación "= =" para comparar ambos ítemes de datos. Como estos ítemes pueden ser de cualquier tipo, pueden ser especialmente objetos de clases definidas por el usuario.
    La cuestión es : ¿Cómo se define la "igualdad" para esos tipos nuevos ? Consecuentemente, para permitir a remove() que trabaje adecuadamente, la lista debería ser usada solamente para tipos que propiamente definan los operadores de comparación (a saber, "= =" y " !="). De otro modo, se usan las comparaciones de "default", lo que llevaría a resultados extraños.

    3.
    La clase CountedList. Una lista contada es una lista que sigue la pista al número de elementos en ella. Así, cuando un ítem de datos es añadido, el número es incrementado en uno, cuando un ítem es eliminado, se decrementa en uno. Nuevamente, no proporcionamos la implementación completa, más bien mostramos un método (append()) y como el número es alterado:
      class CountedList : public List {
        int _count;    // El número de elementos
        ...
      public:
        ...
        virtual void append(const T data) {
          _count++;    // lo incrementa y ...
          List::append(data); // ... usa el append de la lista
        }
        ...
      }
    

    No todos los métodos pueden ser implementados de esta manera. En algunos métodos, uno debe checar si _count necesita ser alterado o no. Sin embargo, la idea principal es que cada método de la lista es solamente expandido (o especializado) para la lista contada.

    4.
    Problema del iterador. Para resolver el problema del iterador se podría pensar en una solución donde el iterador almacene una referencia a su lista correspondiente. En el momento de la creación del iterador, esta referencia es inicializada para referenciar la lista provista. Los métodos del iterador deben ser modificados para usar esta referencia en lugar del apuntador _start.


    next up previous
    Siguiente: Acerca de este documento ... Arriba: Introducción a la Programación Orientada a Objetos Anterior: Referencias
    P. Mueller
    8/31/1997