[UCR]  
[/\]
Universidad de Costa Rica
Escuela de Ciencias de la
Computación e Informática
[<=] [home] [<>] [\/] [=>]
Google Translate

¡No se le meta al Rep!

Adolfo Di Mare




Resumen [<>] [\/] [/\]

Se justifica el uso del ocultamiento de datos como la técnica fundamental de programación usada para disminuir el acoplamiento entre módulos de manera que los programas sean más simples y queden mejor construidos, para reducir la dificultad y el costo de su mantenimiento. We justify the use of data hiding as the fundamental programming technique to reduce module coupling resulting in better built programs that are easier and cheaper to maintain.

      ¡No se le meta al Rep! Mis estudiantes me han criticado porque con mucha frecuencia repito esta frase. Lo que a veces no recuerdan tanto es la razón por la que la digo, pues el uso de abstracción es de primordial importancia para construir programas y sistemas grandes; esta es la mejor forma que conocemos para delegar en otros programadores la implementación de los módulos de un programa. Por eso, la especificación de artefactos de programación es la herramienta principal usada para diseñar programas construidos con componentes reutilizables.

      Como al construir programas es necesario tomar en cuenta todos los casos, el programador tiene una mente acostumbrada a ver el detalle específico en cada situación; algunos programadores tienen dificultad para percibir el bosque pues se pasan trabajando en las hojas. Otras personas tienen la capacidad de entender mejor las cosas si parten primero de lo más "abstracto" para luego caer en lo concreto; generalmente el programador estará acostumbrado a pensar primero en lo más concreto y de ahí saltar a lo más abstracto. La tendencia de construir programas de abajo hacia arriba (bottom-up) que los programadores con frecuencia usamos son buena muestra de que la forma en que pensamos, entendemos y conocemos, como también lo es la tendencia de combinar en un módulo más general la funcionalidad de varios módulos que hacen un trabajo muy específico con el fin de lograr obtener menos módulos que sean de más fácil reutilización. Los programadores llegamos a la abstracción paso a paso, generalizando los casos específicos.

      Si el lector tiene una mente a la que le resultaría más fácil asimilar conocimiento de arriba hacia abajo (top-down), talvez le resulte mejor leer las siguientes secciones de este escrito en orden inverso, comenzando por la [5] de abstracción y terminando en la [1] del Rep; para los demás, como para el autor, la forma de exposición adecuada es la que ya tiene este documento. (La versión HTML de este documento incluye muchos enlaces a las definiciones de la mayor parte de los conceptos aquí discutidos).

 Rep   Los campos de una clase son su Rep; el Rep incluye tanto los campos que aparecen en al definición de la clase como aquellos que forman parte de cualquier otro objeto que se use para implementar la clase.
 Invariante   La invariante es la relación que siempre debe mantenerse entre todos los campos del Rep.
 Implementación   Una implementación es un grupo de instrucciones imperativas a ser ejecutadas por el computador, escritas en uno o más lenguajes de programación. Cada implementación es una de las muchas posibles formas de escribir los algoritmos que constituyen el código fuente de un programa.
 Especificación   En la especificación están definidos todos los servicios que la implementación del módulo es capaz de dar. Toda especificación debe ser completa, correcta y no ambigua. La firma o prototipo del módulo siempre forma parte de la especificación, pues si no se definen los tipos y parámetros con que trabaja el módulo es imposible compilarlo o reutilizarlo.
 Abstracción   La herramienta fundamental que usan los programadores para construir sus programas es la abstracción, que les permite dividir un problema grande en pequeños problemas que son solubles. La solución del problema original es el agregado de las pequeñas soluciones.
Figura 1

1. Rep [<>] [\/] [/\]

      Los campos de una clase son su Rep; el Rep incluye tanto los campos que aparecen en al definición de la clase como aquellos que forman parte de cualquier otro objeto que se use para implementar la clase.

      Cualquier lenguaje de programación permite definir, al menos, variables numéricas, pues los programas lo que hacen es manipularlas para, finalmente, obtener sus resultados. Un programa que no usa variables lo único que puede hacer es emitir mensajes, pero no puede computar resultados; por eso, si un programa es útil necesariamente debe manipular valores almacenados variables. Todo estudiante aprende primero que un computador se puede almacenar valores numéricos y que, usualmente, un número entero está almacenado en 4 octetos ("byte's") consecutivos en la memoria del computador.

Rep Pascal C++ Fortran
m_num
m_den
 TYPE
   Rational = RECORD
     m_num : LONGINT; 
     m_den : LONGINT;
   END;
 class rationalprivate:
     long m_num;
     long m_den;
 };
 
 
 VAR
   cero : Rational;

  rational cero;
 INTEGER Rn_0;
 INTEGER Rd_0;
Figura 2

      Al construir programas con gran frecuencia es necesario juntar varias variables para que, todas juntas, representen la información de un objeto. Por ejemplo, un número racional se puede representar juntando 2 variables enteras: en una se almacena el numerador del número racional y en la otra su denominador. En la jerga ("argot") de los programadores se diría que esos 2 valores han sido "agregados", usando la "composición de clases", para obtener el objeto "rational" que permite almacenar el valor de un número racional, como se muestra en la Figura 2. El lenguaje Pascal sí permite agrupar varios campos usando la construcción sintáctica "TYPE" y en C++ se usan clases.

      La palabra "tipo" es sinónimo de "clase" en C++. Si la sintaxis del lenguaje de programación permite usar tipos, o clases, para el programador resulta más fácil construir sus programas porque puede definir variables usando clases. Por ejemplo, en Pascal o C++ la variable "cero" incorpora 2 campos, llamados "m_num" y "m_den", en los que están almacenados el numerador y denominador del número racional, valor que el programador puede acceder usando la notación del punto: "cero.m_num". Los primeros lenguajes de programación, como Fortran, estaban diseñados para manipular únicamente variable numéricas por lo que el programador debía recurrir a trucos sintácticos para poder juntar variables que representan un valor compuesto. Por eso el programador Fortran debe recurrir a convenciones sintácticas como la usada en la Figura 2, en que el nombre de la variable tiene el prefijo "R" como indicación de que el valor ahí almacenado es una parte de un número racional en donde el denominador tiene en el nombre una "d" mientras que el numerador tiene una "n"; como en Fortran los identificadores deben ser cortos, con un máximo de 6 letras, el racional "CERO" a fin de cuentas queda almacenado en 2 variables separadas llamadas "Rn_0" y "Rd_0". Esto es muy engorroso, dificulta la construcción de programas y justifica el uso de las clases en los modernos lenguajes de programación (en versiones más nuevas de Fortran ya están incorporadas construcciones sintácticas que facilitan la programación).

      En el ejemplo de la Figura 2 he usado una convención para los nombres de los campos que consiste en poner el prefijo "m_" a cada identificador. A la costumbre de decorar identificadores con prefijos se le llama "notación húngara", como reconocimiento a Simonyi, el ingeniero húngaro que lo propuso a finales del Siglo XX [SIM-1999]. Esta manera de nombrar los campos de una clase es utilizado por muchos programadores Microsoft, por lo que he decidido usarla yo también.

      Debido a la importancia de los datos en los programas se creó la Programación Orientada a los Objetos, conocida como OOP por sus siglas en inglés. OOP es un paradigma de programación centrado en los datos llamado. En OOP se les llama instancias a los valores almacenados en la memoria del computador; esta palabra tiene un significado más amplio que el de la palabra "variable" porque algunas instancias existen en memoria dinámica por lo que no tienen asociado un identificador sino que se accede a su valor indirectamente a través de un puntero. Un lenguaje que tiene OOP permite usar encapsulamiento para definir en la misma clase los campos de un objeto junto con las operaciones usadas para manipular al objeto. Además, OOP implica también el uso de herencia, que es una facilidad sintáctica de lenguaje que permite agregar campos a un clase al definir otra, que es la clase derivada (todos estos conceptos están explicados en muchos artículos, como por ejemplo en [DiM-1991]).

      La palabra objeto tiene 2 significados diferentes en programación. En algunos contextos el significado de la palabra objeto es el mismo de clase, o de tipo, pero en otros significa instancia. Por eso, la programación por objetos puede interpretarse como "programación orientada a las instancias" o como "programación orientada a las clases". El lector debe conocer ambos concepto de manera que pueda discernir cual es el significado que el autor del escrito usa y debe también recordar que a veces la palabra objeto representa eso mismo, o sea, que representa un objeto, definido en el diccionario como sinónimo de cosa: "Todo lo que tiene entidad, ya sea corporal o espiritual, natural o artificial, real o abstracta".

      Todo este preámbulo sirve para decir, simplemente, que en la memoria del computador están almacenados valores que son instancias de una clase o tipo. Lo usual es que estos valores codificados en binario estén localizados en posiciones consecutiva de memoria. Ese es el "Rep" del objeto.

      El vocablo "Rep" es usado por los autores Liskov y Gutag en su libro sobre abstracción y especificación de programas [LG-1986]. La palabra "Rep" es un acrónimo de "representación"; sobre su significado estos autores dicen: "Para implementar una abstracción de datos, seleccionamos una representación, o Rep, para los objetos y luego implementamos los constructores para inicializar la representación apropiadamente y los métodos que usan o modifican esa representación apropiadamente". En otras palabras, el Rep de una clase son sus campos; lo usual es que el Rep sea privado y cuando el programador lo accesa... ¡se le mete al Rep!

   L                                        NULL
 +---+    +-----+---+   +-----+---+   +-----+---+
 | *-|--->|  1  | *-|-->|  2  | *-|-->|  3  | ∅ |
 +---+    +-----+---+   +-----+---+   +-----+---+
m_first   m_val m_next
Figura 3

      En la Figura 3 se muestra un Rep que sirve para implementar una lista la de números enteros. Debido a que esta lista almacena sus valores en memoria dinámica, en su Rep hay punteros. A primera vista pareciera que la lista tiene sólo un campos en el Rep: "m_first"; sin embargo, también forman parte del Rep todos los nodos que contienen los valores almacenados en la lista. En este caso, el Rep de los nodos de la lista contiene 2 campos: "m_val" junto con "m_next", y el segundo es un puntero al siguiente nodo. Si queremos que el programador "No se le meta al Rep", querremos también evitar que use los campos del nodo de la lista (el operador C++ "sizeof()" sólo calcula el tamaño de la parte del Rep que no está en memoria dinámica y no toma en cuenta el resto del Rep compuesto de nodos).

<=========================================================>
|                                           m_prev m_next |
|    +-+-+     +-+-+     +-+-+     +-+-+        +-+-+     |
<===>|*|*|<===>|*|*|<===>|*|*|<===>|*|*|<======>|*|*|<====>
     +-+-+     +-+-+     +-+-+     +-+-+        +-+-+
     | 1 |     | 2 |     | 3 |     | 4 |          L
     +---+     +---+     +---+     +---+          ^
       ^       m_val                 ^           /|\
       |                             |            |
   L.first()                      L.last()      L.end()
Figura 4

      Debido a que la escogencia del Rep la hace el programador al programar su implementación, en general ocurre que hay muchas formas diferentes de implementar el mismo objeto. Por ejemplo, en la Figura 4 se muestra un Rep que sirve para implementar la lista de la biblioteca STL de C++ "std::list", formada por 2 campos: "m_prev" junto con "m_next". Estos campos sirven para formar una lista circular doblemente enlazada (no siempre es fácil hacer un dibujo usando únicamente las letras del alfabeto ASCII; en la Figura 4 el valor que encabeza la lista es la cajita de la derecha, pues el truco que permite implementar eficientemente una lista "std::list" es usar un cabezal en la última posición de la lista).

      ¿Qué es el Rep? El Rep son los campos de la clase. Generalmente esos campos son privados como se explica en la siguiente sección.


2. Invariante [<>] [\/] [/\]

      La invariante es la relación que siempre debe mantenerse entre todos los campos del Rep.

      A primera vista parece que cualquier valor almacenado en la memoria de un computador es un valor válido, pero esto en realidad no es así para la mayor parte de los objetos. Talvez la única excepción a esta regla son los números enteros, de tipo "int" o "long", en la que es todo patrón de bit's almacenado siempre representa un número entero válido. Pero para variables un poquito más complejas, como por ejemplo los números de punto flotante, de tipo "float" o "double", ya la regla sí se cumple. Muestra de este hecho es que existen muchos patrones de bits que al ser almacenados en una variable de punto flotante resultan en valores inválidos, pues no cumplen con los requisitos de signo, mantisa y exponente de un número de punto flotante. Las constantes C++ que representan estas valores inválidos son "std::numeric_limits<float>::quiet_NaN()", que no genera ninguna señal cuando el valor es leido o grabado en memoria, y la constante "std::numeric_limits<float>::signaling_NaN()" que sí la genera. Por eso, en cuanto un objeto tiene más campos, la relación que debe existir entre esos campos se hace cada vez más complicada.

/// ¿ x < y ?
inline bool operator < (const rational &x, const rational &y) {
    return (x.m_num * y.m_den) < (x.m_den * y.m_num);
/*  Nota:
    Una desigualdad de fracciones se preserva siempre que se
    multiplique a ambos lados por un número positivo. Por eso:
          [a/b] <       [c/d]   <==>
    [(b*d)*a/b] < [(b*d)*c/d]   <==>
          [d*a] < [b*c]

    [a/b] > [c/d] <==> [(b*d)*a/b] > [(b*d)*c/d] <==> [d*a] > [b*c]

    Debido a que el denominador siempre es un número positivo, el
    trabajo de comparar 2 racionales se puede lograr haciendo 2
    multiplicaciones de números enteros, en lugar de convertirlos
    a punto flotante para hacer la división, que es hasta un orden
    de magnitud más lento.
*/
//  return double(x.m_num) / double(x.m_den) < double(y.m_num) / double(y.m_den); 
}  // operator <
Figura 5

      Para la clase "rational" de números racionales parece relativamente sencillo definir cuál es la relación que debe existir entre el numerador y denominador: basta que ambos sean números enteros, lo que de hecho siempre ocurre pues el tipo de los campos "m_num" y "m_den" es entero. Sin embargo, también es necesario asegurar que el denominador no sea nulo, pues la división por cero no está definida. Al implementar la clase, se hace necesario evitar racionales con denominador negativo, pues para esos racionales cuesta más implementar la operación de comparación menor "<", como se muestra en el algoritmo de la Figura 5. Para quien usa la clase "rational" no es importante saber detalles de implementación como éstos y, en buena teoría, esas consideraciones debieran quedar ocultas; al programador cliente de una clase no le concierne cuáles son los detalles de implementación que se tomaron en cuenta al construirla. Pero para quien construye la clase es necesario que exista la garantía de que los valores almacenados en el Rep mantienen la relación que debe existir entre esos campos, pues de lo contrario la implementación de las operaciones de la clase no funcionaría correctamente, como ocurriría si se usara el algoritmo de la Figura 5 para comparar 2 racionales cuyos denominadores son números negativos (al final de este documento se incluyen las direcciones Internet en donde está el código fuente de la clase "rational"; [DiM-1994] contiene una descripción de una implementación para Pascal de esta clase).

      Para almacenar en el computador el valor de cualquier objeto es necesario poner juntos varios campos, y escoger una o más formas de interrelación entre ellos. Esta organización de los campos del objeto constituye su "representación privada", resumida por el acrónimo Rep [LG-1986]. A la relación que siempre deben cumplir los campos del Rep se la conoce como la invariante del tipo de datos; la ventaja de que un objeto cuente con operaciones es que quien las programa puede asegurarse de que el valor que queda almacenado después de cada operación siempre es adecuado, o sea, que siempre se cumplirá la invariante después de invocar a una operación de la clase. Cabe destacar que el vocablo "invariante" tiene 2 significados en programación de computadores:

  1. La relación que se siempre debe existir entre los campos que forman un objeto. Esta es la invariante de las clases.
  2. La condición que se establece como de entrada a un ciclo "while". Este tipo de invariante se usa para demostrar matemáticamente que un programa es correcto, discusión que no es directamente relevante aquí.

      Para las clases, la invariante es un predicado que debe ser verdadero para los campos del objeto. En otras palabras, una invariante se puede expresar como una fórmula booleana que debe ser siempre verdadera para cualquier instancia de una clase pues, si la invariante fuera falsa, el objeto estaría mal construido y, por ende, el usar sus operaciones produciría resultados erróneos e impredecibles. Los métodos de una clase siempre restablecen la invariante de la clase antes de terminar su trabajo, porque siempre deben dejar el objeto bien construido. Las únicas operaciones que se pueden aplicar a instancias que no cumplen con la invariante son los constructores, que son precisamente los encargados de establecer la invariante de una clase como parte de la inicialización de la instancia. Para definir y especificar la invariante de una clase se pueden seguir varios caminos:

  1. Definición matemática de la invariante.
  2. Descripción en palabras de la invariante.
  3. Implementación de la operación "check_ok()".
  4. Implementación del método "Ok()".

      Usar matemática para definir invariantes es atractivo para los programadores que tienen habilidades analíticas pronunciadas. Desafortunadamente, la mayoría de los programadores considera complicado el lenguaje matemático, aunque permite la mayor exactitud posible porque una fórmula matemática no es ambigua. Para la clase "rational" un predicado matemático que establece la invariante podría ser el siguiente:
    (this->m_den > 0) && (1 == mcd(this->m_num, this->m_den))
Aquí la función "mcd(n,m)" sirve para calcular el máximo común divisor de 2 enteros.

      La siguiente manera de definir la invariante es usar el lenguaje natural (por ejemplo, español o inglés). Esta forma coloquial puede resultar en una definición muy imprecisa de la invariante porque generalmente los programadores no son expertos en la redacción de texto técnico; muchos consideran hasta denigrante aquellas actividades que puedan relacionarse con "escribir documentación". Sin embargo, el lenguaje natural tiene la ventaja de que es más fácil de entender:

  1. El denominador del racional siempre es un número mayor que cero.
  2. El número racional siempre está simplificado, o sea que el numerador y el denominador siempre son números primos relativos.
  3. Cero se representa como "(0/1)".

      Es saludable mantener la documentación de la invariante, pero cuesta lograr que la documentación esté actualizada porque con frecuencia los programadores modifican el programa para hacerle mejoras y, debido a la premura del tiempo, dejan de lado la importante tarea de actualizarle también la documentación.

      La siguiente opción para definir la invariante es programarla. Para hacerlo se pueden seguir 2 caminos muy parecidos: usar un método encapsulado en la definición de la clase o usar una función amiga de la clase, o sea, usar una operación para el objeto. Para los números raciones, la primera forma requiere implementar el método "rational::Ok()", mientras que la alternativa es programar la función amiga "check_ok(const rational& r)". Ambas alternativas son funcionalmente equivalentes, pero usar la función "check_ok()" tiene la ventaja de que evita forzar al programador cliente de una clase a implementar el método "Ok()" en aquellos casos en que en el Rep se usan otros objetos, como siempre ocurre al implementar objetos contenedores como la lista o el árbol.

/** Verifica la invariante de la clase \c rational.

    \par <em>Rep</em> Modelo de la clase:
    \code
    +---+
    | 3 | <==  m_num == numerador del número racional
    +---+
    |134| <==  m_den == denominador del número racional
    +---+
    \endcode

    - http://www.di-mare.com/adolfo/binder/c03.htm#k1-Rep

    \remark
    Libera al programador de implementar el método \c Ok()
    - http://www.di-mare.com/adolfo/binder/c04.htm#sc11
*/
bool check_ok( const rational& r ) {
    if ( &r != 0 ) {
        // Ok
    }
    else {
        /// - Invariante: ningún objeto puede estar almacenado en la posición nula.
        return false;
    }

    if ( r.m_den > 0 )  {
        // Ok
    }
    else {
        /// - Invariante: el denominador debe ser un número positivo.
        return false;
    }
    if (r.m_num == 0) {
        if ( r.m_den == 1 ) {
            /// - Invariante: el cero debe representarse con denominador igual a "1".
            return true;
        }
        else {
            return false;
        }
    }
    if ( mcd(r.m_num, r.m_den) == 1 ) {
        // Ok
    }
    else {
        /// - Invariante: el numerador y el denominador deben ser primos relativos.
        return false;
    }
    return true;
} // check_ok()
Figura 6

     En la Figura 6 se muestra la implementación de la operación que verifica la invariante para los números racionales. A primera vista, el estilo de programación de la invariante parece innecesariamente extenso; por ejemplo, en lugar de usar directamente la expresión booleana "(r.m_den <= 0)" se usa un "if()" que tiene la condición positiva vacía con el comentario "// Ok". Esta forma de expresar la invariante es preferible porque las personas entendemos mejor las afirmaciones positivas y al usar este estilo queda bien expresado cuál es la invariante, "(r.m_den > 0)" en este caso, de manera que el algoritmo programado indica que cuando eso no se cumple el valor que "check_ok()" retornará es "false".

bool check_ok ( const rational & r ) [friend]
Verifica la invariante de la clase rational.

Rep Modelo de la clase:
        +---+
        | 3 | <==  m_num == numerador del número racional
        +---+
        |134| <==  m_den == denominador del número racional 
        +---+
Comentarios:
  • Libera al programador de implementar el método Ok()
  • Invariante: ningún objeto puede estar almacenado en la posición nula.
  • Invariante: el denominador debe ser un número positivo.
  • Invariante: el cero debe representarse con denominador igual a "1".
  • Invariante: el numerador y el denominador deben ser primos relativos.
Definición en la línea 34 del archivo rational.cpp.
Figura 7

      Doxygen es una herramienta que genera la documentación de programas extrayéndola del código fuente [VANH-2004]. Al implementar la función "check_ok()" se han incluido comentarios Doxygen de manera que esa misma implementación también tiene la la documentación en lenguaje natural para definir la invariante en palabras lo que facilita mantenerla actualizada; esta forma de trabajar mantiene juntos el algoritmo que verifica la invariante con la descripción de lo que la invariante debe hacer. En la Figura 7 se muestra la documentación generada por Doxygen, la que queda en la sección de funciones amigas de la clase "rational". Debido a que la invariante es la relación que siempre debe mantenerse entre todos los campos del Rep, al especificar la función "check_ok()" que verifica la invariante para una clase siempre es necesario y correcto hablar de los campos del Rep de la clase.

class Viejillo {
private: // Rep
    string m_nombre;
    int    m_edad;
public: // Get-teadores
    const string get_nombre()   const;
    const int    get_edad()     const;
public: // Set-teadores
    void set_nombre(const string& n);
    void set_edad(int i);

    friend bool check_ok( const Viejillo & V );
};

bool check_ok( const Viejillo & V ) {
    if ( check_ok(V.m_nombre) ) { /* Ok */ }
    else {
        return false;
    }
    if ( check_ok(V.m_edad) ) { /* Ok */ }
    else {
        return false;
    }
    return true;
}
Figura 8

      Ocurre en muchas ocasiones que una clases representa una entidad cuyos datos están almacenados en una base de datos. Estas clases contienen campos con nombres similares a "Edad", "Nombre", "Teléfono", "Código de cliente", etc. Muchos ambientes de desarrollo de programas (IDE, por sus siglas en inglés) generan automáticamente los métodos tipo "get()" y "set()" (también llamados get−teadores y set−teadores) que permiten usar los campos almacenados en el objeto. Para estos casos es posible escribir un programa que valida el Rep validando cada uno de los campos de la clase. Debido a que es tan usual usar este tipo de clases, muchos programadores omiten programar la invariante para la clase argumentando que la implementación "es obvia". Siempre es saludable y recomendable implementar la invariante para una clase porque al hacerlo el programador comprende mejor cómo funciona la clase, lo que le permite reducir la cantidad de errores en la implementación de las operaciones para la clase.

template <class T>
inline bool check_ok( const T& V ) {
    return true;
}
bool check_ok( const list & L ) {
    if ( &L != 0 ) {
        // Ok
    }
    else {
        /// - Invariante: ningún objeto puede estar almacenado en la posición nula.
        return false;
    }

    if ( L.m_first == 0 ) {
        /// - Invariante: la lista vacía siempre se representa
        ///   con el puntero nulo en \c m_first.
        return true;
    }

    list::node * p = L.m_first;
    while ( p != 0 ) {
        if ( check_ok( p->m_val ) ) {
            // Ok
        }
        else {
            /// - Invariante: El valor \c "m_val" almacenado en un nodo
            ///   de la lista siempre debe ser un valor correcto.
            return false;
        }
        p = p->m_next;
    }
    return true;
}
Figura 9

      La Figura 9 contiene la implementación de "check_ok()" para una lista sencilla, con un Rep como el que se muestra en el diagrama de la Figura 3. Como la lista es un contenedor, es necesario verificar que los valores almacenados "m_val" son correctos, lo que obliga a invocar la operación "check_ok()" para cada uno de ellos. Esto implica que si el programador cliente de la lista no ha implementado el "check_ok()" para su clase, el compilador emitirá un mensaje de error al no encontrar esa función. Si el programador no quiere implementar "check_ok()", una solución parcial es crear una función que siempre retorna "true"; la otra, en el caso del lenguaje C++, es implementar la lista usando programación genérica, pues C++ no instancia la plantilla a menos que sea invocada en el programa por lo que, si el programador cliente no invoca "check_ok(const list &)" el compilador tampoco invocará esta operación para el valor almacenado en la lista.

      La ventaja más importante de definir la invariante implementando la operación "check_ok()" es que para cualquier programador el trabajo de escribir el algoritmo que verifica la invariante es similar a cualquier otro trabajo de programación: para todo programador es natural escribir programas. Además, una vez programada, la invariante queda definida con un precisión muy grande, casi matemática, pues el significado de un programa siempre es único. A estas ventajas hay que agregar la posibilidad de incluir documentación Doxygen para describir con palabras el Rep y la interrelación de sus campos, lo que contribuye a mejorar la documentación de la clase. La completitud, precisión y facilidad de implementación de la operación "check_ok()" son la razón por la que es la mejor forma de definir la invariante de una clase; los programadores debieran adquirir el buen hábito de implementar "check_ok()" desde el principio, cuando diseñan sus clases. La razón por la que uso la función amiga "check_ok()" en lugar del método "Ok()" para verificar la invariante de una clase es evitar forzar al programador a implementar el método "Ok()" en todas sus clases; puede que otros opinen que dejarle este grado de libertad al programador es un error, en cuyo caso la ruta lógica sería hacer obligatorio el uso e implementación del método "Ok()".

template <class T>
class list_node {
private: // todo es privado para un nodo
    template <class T> friend class list;
private:
    T              m_val;   ///< Valor almacenado en la lista
    list_node<T> * m_next;  ///< Puntero al siguiente nodo
// ...
};

template <class T>
class list {
public:
    // ...
    bool Ok() const;
    // check_ok() sólo invoca a Ok(), que hace todo el trabajo
    friend bool check_ok( const list & L ) { return L.Ok(); }
private: // Rep de la lista
    list_node<T> * m_first; ///< Puntero al primer nodo de la lista 
};

template <class T>
bool list::Ok() const {
    // ...
    return true;
}
class TITAN {
// ...
    friend bool check_ok(const TITAN &);

    /// Usa \c check_ok() para verificar la invariante de la clase
    bool Ok() const { return check_ok(*this); }
// ...
};
Figura 10

      Como se muestra en la parte alta Figura 10, debido a limitaciones del lenguaje C++, si se usan plantillas a veces no es posible lograr que el compilador acepte la función amiga "check_ok()" por lo que no queda más que implementar "Ok()" como un método privado que únicamente "check_ok()" invoca. En caso contrario es más cómo implementar "check_ok()" como se muestra en la parte baja de la Figura 10.

              -<-----
             /       \
   L        \|/       \
 +---+    +--v--+---+  |
 | *-|--->|  1  | *-|-/
 +---+    +-----+---+
m_first   m_val m_next
Figura 11

      Desafortunadamente, si un objeto no cumple con su invariante, puede ser que "check_ok()" no funcione. Por ejemplo, si la lista tiene los punteros quebrados es posible que "check_ok()" se encicle y nunca retorne (como con la lista de la Figura 11), o que examine posiciones de memoria inválidas que la lleven a retornar un valor inválido. Para objetos más sencillos, muchos de los que no usan memoria dinámica, la implementación de "check_ok()" no presenta estos problemas pero, en general, se puede afirmar que, si la instancia es un objeto bien construido, al verificar la invariante "check_ok()" retornará "true" pero, en caso contrario, es posible que retorne un valor incorrecto o que nunca retorne.

      Como la invariante forma parte de la implementación, casi siempre la responsabilidad de preservar la invariante es del programador que implementa la clase pues si se permite al programador cliente acceso liberal a los campos del Rep con facilidad podrá quebrantar el delicado equilibrio de la invariante, lo que produciría resultados incorrectos e impredecibles. Además, al modificar una clase es necesario hacerlo coherentemente, de manera que se preserve la invariante. Por eso, los lenguajes de programación modernos permiten proteger los campos del Rep declarándolos como privados, de manera que el programador cliente tenga acceso al objeto completo, pero no a sus detalles internos de implementación. A esta facilidad sintáctica se le conoce como ocultamiento de datos, pues libera al programador cliente de la clase de lidiar con sus detalles de implementación al no permitirle usar directamente los campos del Rep. O sea: ¡el ocultamiento de datos sirve para que uno no se le meta al Rep!

      El ocultamiento de datos es la técnica fundamental de programación que permite disminuir la cohesión entre módulos, lo que resulta en programas más simples y mejor construidos, lo que reduce la dificultad y el costo de su mantenimiento.

      Si el lenguaje de programación permite declarar los campos de un objeto como privados será trabajo del compilador evitar que el programador cliente de la clase se le meta al Rep, lo que permitirá construir en capas o partes los artefactos de programación. El ocultamiento de datos es el mecanismo principal que permite construir programas complejos a partir de módulos más simples.


3. Implementación [<>] [\/] [/\]

      Una implementación es un grupo de instrucciones imperativas a ser ejecutadas por el computador, escritas en uno o más lenguajes de programación. Cada implementación es una de las muchas posibles formas de escribir los algoritmos que constituyen el código fuente de un programa.

      Para construir una casa es necesario levantar las paredes y poner el techo. Sin embargo, la construcción de una casa tiene muchas otras actividades que a veces pasan desapercibidas. Lo mismo ocurre con la construcción de programas, pues muchas personas piensan que la codificación de los algoritmos es la totalidad del trabajo, olvidando las necesarias actividades de planificación y diseño que preceden a la escritura de los algoritmos, o todas las actividades para poner en funcionamiento los programas y sistemas después de que están construidos.

      Una parte muy importante de la implementación es la verificación de que el algoritmo es correcto. Nos es posible hacer pruebas de módulos exhaustivas, por lo que al hacer prueba de programas necesariamente hay que escoger algunos pocos casos de prueba representativos. También se puede invertir esfuerzo en desarrollar una prueba matemática de la correctitud de los algoritmos, pero con frecuencia la complejidad de la prueba es mucho mayor que la del algoritmo, por lo que el problema "pequeño" de probar el programa queda transformado en un problema "grande" de índole matemático. Por eso, la solución más popular para probar programas es el uso de prueba unitaria de programas, como la propuesta en "JUnit", resumida en la política de "haga la prueba y luego el programa" [KG-2000].

      Para la mayor parte de los programadores la codificación de algoritmos es la parte más interesante de la construcción de programas; los programadores trabajamos creando mundos virtuales, en donde todas las variables coexisten bajo nuestro perfecto control: esta es la parte más creativa del trabajo del programador. Para construir programas los programadores usamos muchos trucos, desde el uso de estructuras de datos y algoritmos avanzados, hasta la aplicación de convenciones como las descritas en [DiM-1988], que facilitan la labor de programación y mantenimiento del programa.

      Es posible describir muchas reglas que resultan en una mejor implementación, como por ejemplo las convenciones descritas en:

          • [DiM-1988] ==> http://www.di-mare.com/adolfo/p/convpas.htm
          • [KNDK-1997] ==> http://java.sun.com/docs/codeconv/CodeConventions.pdf
          • [Hunt-1998] ==> http://weblogs.asp.net/lhunt/pages/CSharp-Coding-Standards-document.aspx

      Yo sigo un camino más sencillo pues les pido a mis estudiantes que al escribir su implementación, deben asegurarse de cumplir siempre con estas 3 políticas de programación, que considero las más importantes:

  1. Es muy importante que el programa esté correctamente indentado y que las instrucciones estén debidamente espaciadas.
  2. Es muy importante que en todo el programa haya buena documentación interna.
  3. Uso de Doxygen para la especificación de todos los métodos, funciones y campos de la clase.

      Mis asistentes saben cuál es la forma de calificar proyectos programados. Específicamentes, se concentran en encontrar fallas que quiebren estas 5 reglas:

  1. La falta de cualquier especificación debe ser castigada fuertemente.
  2. Correcta indentación del código fuente.
  3. Correcto espaciado del código fuente.
  4. Código fuente escrito de manera que sea legible y claro.
  5. Uso de identificadores significativos.

      Es fácil detectar cuál es la implementación: es la parte del programa en donde están las instrucciones imperativas que el computador debe ejecutar. Por supuesto, El Rep forma parte de la implementación de una clase, pues debido al ocultamiento de datos no es visible para los programadores cliente de la clase.


4. Especificación [<>] [\/] [/\]

      En la especificación están definidos todos los servicios que la implementación del módulo es capaz de dar. Toda especificación debe ser completa, correcta y no ambigua. La firma o prototipo del módulo siempre forma parte de la especificación, pues si no se definen los tipos y parámetros con que trabaja el módulo es imposible compilarlo o reutilizarlo.

      El principio más importante que hay que recordar al redactar cualquier especificación es que en la especificación se habla del QUÉ, y no se habla del CÓMO, para que quien realiza la implementación tenga la libertad de seleccionar los algoritmos y estructuras de datos óptimos para resolver el problema. Con frecuencia, los programadores novatos confunden la documentación interna, que es la que se incluye en la implementación para aclarar las partes que pueden ser confusas en un algoritmo, con la definición de los servicios que brinda un módulo descritos en su especificación. Es un error mencionar en la especificación cuestiones que corresponden a la implementación.

      Toda especificación incluye la definición de los objetos con que trabaja el módulo junto con la descripción de la funcionalidad implementada. Además, en muchos casos también se incluyen ejemplos de uso para facilitarle al programador cliente la reutilización.

Descripción
Esta parte de la especificación está escrita en lenguaje natural, como inglés o español, y es la parte en donde el programador dice para qué sirve el módulo. Es usual incluir una descripción corta y también una descripción detallada.
Prototipo
El compilador solo necesita el prototipo o firma del módulo para compilarlo, pues ahí están definidos los de tipos de datos y parámetros usados. En el prototipo también queda definido el tipo de objeto que retornan métodos o funciones.
Ejemplo de uso
Muchas veces un ejemplo de uso le permite al programador cliente comprender más rápido cuál es la funcionalidad del módulo. Por ejemplo, incluir datos de prueba como parte de la especificación es una buena idea [DiM-2008].

      Algunos programadores confunden el concepto de "documentación" con el de "especificación", pues muchas veces han tenido que implementar módulos que han sido especificados por otras personas. Sucede que la mayor parte de los programas son diseñados por profesionales cuya remuneración es relativamente alta para luego delegar en programadores más baratos el trabajo restante de implementación; esto es más común en ambientes en que los algoritmos utilizados son sencillos, como ocurre en la mayor parte de las aplicaciones de sistemas de información. En estos ambientes de trabajo la especificación de módulos es la herramienta fundamental para separar las funciones que reducen el costo de construir programas. Dicho de otra manera, la especificación de módulos es el instrumento que permite la maquila de programas.

long ADH::mcd( long x, long y )
Calcula el Máximo Común Divisor de los números "x" y "y".
  • mcd(x,y) >= 1 siempre.
  • MCD <==> GCD: Greatest Common Divisor.
Precondición:
   (y != 0)

Comentarios:
   Se usa el algoritmo de Euclides para hacer el cálculo.

Ejemplo:
    2*3*5 == mcd( 2*2*2*2 * 3*3 * 5*5, 2*3*5 )
       30 == mcd( -3600, -30 )
Definición en la línea 131 del archivo rational.cpp.
Figura 12

      Debido a que los programas son algoritmos que manipulan datos, es necesario especificar programas y especificar datos. La especificación de datos se aplica a las clases, sus variables y sus instancias, y la especificación procedimental se aplica a los algoritmos (la abstracción de iteración es un caso particular de la abstracción de datos). Si el lenguaje permite la programación genérica o el uso de plantillas, es necesario ampliar las especificaciones para estos casos más generales, lo que resulta en la abstracción por parametrización que es una generalización de la abstracción por especificación; en [LG-1986] se explica con gran detalle todo esto. Aquí desarrollo el enfoque más práctico, y explico con ejemplos cómo escribir una buena especificación.

      La forma sencilla de "no metérsele al Rep" en la especificación es evitar mencionar lo que no es público ("public"). Por ejemplo, la clase "node" es una clase interna de la clase "list", por lo que está definida como privada ("private"). Por eso, en ninguna especificación de las operaciones públicas de "list" se debe mencionar "node", o a cualquier otro campo o método privado. Sin embargo, si un método o función es privado en su especificación sí se puede hablar del Rep, como ocurriría con el método "list::last()" que retorna un puntero al último nodo de la lista (este método, por supuesto, retorna un puntero, y hace un trabajo diferente a "list::first()" que retorna un iterador que permite usar el último valor de la lista, pues la clase "list::iterator" es una clase pública de la clase "list").

      A los novatos que me lo preguntan yo siempre les digo: "la especificación comienza con el primer caracter de la documentación Doxygen de cada método o función, y termina donde está el corchete abierto "{" que marca la implementación del algoritmo en el código fuente del programa".

(***) En las especificaciones sí es conveniente incluir un ejemplo de uso
/** Calcula el Máximo Común Divisor de los números \c "x" y \c "y".
    - <code> mcd(x,y) >= 1 </code> siempre.
    - MCD <==> GCD: <em> Greatest Common Divisor </em>.

    \pre
    <code> (y != 0) </code>

    \remark
    Se usa el algoritmo de Euclides para hacer el cálculo.

    \par Ejemplo:
    \code
    2*3*5 == mcd( 2*2*2*2 * 3*3 * 5*5, 2*3*5 )
       30 == mcd( 3600, 30 )
    \endcode
*/
long mcd(long x, long y);
(***) En la especificación se habla del QUÉ
SI==> /// Ordena el vector \c "V[]" de dimensión \c "n"
      void Sort( unsigned n, T V[] );
(***) Es un error hablar del CÓMO en la especificación
NO==> /// Intercambia los valores hasta que ya no están desordenados
      void Sort( unsigned n, T V[] );
(***) En la especificación no se habla del Rep
NO==> /// Agrega el nodo al final
      void list::push_back( const T& v );
(***) Es un error hablar del Rep cuando se redacta la especificación
NO==> /// Si <code> m_last!=0 </code> borra el nodo del frente
      void list::pop_front();
SI==> /// Elimina el primer valor del contenedor
      /// \pre <code> !empty() </code>
      void list::pop_front();
(***) Si la clase "lista" contiene "nodos" en su parte privada es incorrecto hablar de "nodos" en la especificación
(***) Si la clase "lista" usa "punteros" en su parte privada es incorrecto hablar de "punteros" en la especificación
(***) Es un error hablar de la implementación cuando se redacta la especificación
NO==> /// Usa cirugía de punteros para poner al último nodo de primero
      void list::z_splice();
SI==> /// Traslada el último valor de \c "*this" y lo coloca de primero
      /// \remark No copia el valor
      /// \pre <code> !empty() </code>
      void list::z_splice();
(***) Es incorrecto omitir información que el programador cliente necesita para poder usar la clase, método o función
(***) Debe incluir suficiente información para que el programador cliente pueda usar la clase, método o función
NO==> /// Esta función traslada valores de la lista
      /// void z_splice();
SI==> /// Traslada el último valor de \c "*this" y lo coloca de primero
      /// \remark No copia el valor
      /// \pre <code> !empty() </code>
      void list::z_splice();
(***) Es incorrecto mencionar los campos privados del Rep de una clase en la especificación de cualquiera de sus métodos
NO==> /// Si <code> m_first!=0 </code> borra el nodo del frente
      void list::pop_front();
SI==> /// Elimina el primer valor del contenedor
      /// \pre <code> !empty() </code>
      void list::pop_front();
(***) Es un error omitir los parámetros al hacer la especificación
NO==> /// Agrega valores a la lista
      void push_back( list& L, T v );
(***) Es obligatorio incluir los parámetros en la especificación
SI==> /// Agrega una copia del valor \c "v" al final de \c "L"
      void push_back( list& L, const T& v );
(***) Es un error omitir el prototipo al hacer la especificación
(***) Es obligatorio incluir el prototipo en la especificación
NO==> /// Agrega una copia del valor \c "v" al final de \c "L"
SI==> /// Agrega una copia del valor \c "v" al final de \c "L"
      void push_back( list& L, const T& v );
(***) Es un error confundir métodos y funciones
NO==> /// \remark push_back() es una función...
     void list::push_back( const T& v );
SI==> /// \remark push_back() es un método...
      void list::push_back( const T& v );
NO==> /// \remark push_back() es un método...
      void push_back( list& L, const T& v );
SI==> /// \remark push_back() es una función...
      void push_back( list& L, const T& v );
(***) Las funciones no son métodos porque no tienen un parámetro \c "*this"
(***) Los métodos no son funciones porque si tienen su parámetro \c "*this"
(***) En las especificaciones sí es conveniente incluir un ejemplo de uso

5. Abstracción [<>] [\/] [/\]

      La herramienta fundamental que usan los programadores para construir sus programas es la abstracción, que les permite dividir un problema grande en pequeños problemas que son solubles. La solución del problema original es el agregado de las pequeñas soluciones.

 Ejemplos de abstracción:
 =======================
 - Pila ==> Push() + Pop()           \
 - Número ==> [ + - * / ]            |___
 - Cola ==> Enqueue() + Dequeue()    |   |
 - Matriz ==> A(i,j)                 ^   ^
 - Vector ==> V(j)                   Silla 
Figura 13

      Desde un punto de vista ontológico, la abstracción está compuesta por lo esencial y relevante, dejando de lado los demás detalles. Por eso entendemos que una pila está determinada por sus operaciones más importantes, como lo son Stack.Push()+Stack.Pop() para la pila, y la figura de una silla sólo tiene su respaldar y sus patas, como en la imagen de la Figura 13. Desde un punto de vista práctico, en programación la abstracción son las operaciones públicas de una clase, o sea, los métodos encapsulados en la definición de la clase junto con las operaciones amigas de la clase. Los conceptos principales de abstracción están definidos en el diccionario:

      Una clase tiene muchas operaciones, pero unas cuantas son las más importantes. Las demás operaciones son necesarias, como por ejemplo los constructores y destructores, pero no forman parte de la idea que tenemos cuando pensamos en el objeto. Por ejemplo, al pensar en el concepto de "lista" sabemos que incluye un método para agregarle valores y otra para eliminarle, pero no pensamos que sea necesario el método que le elimina sólo los valores almacenados en una posición par; tampoco pensamos en el constructor o los copiadores. Por eso, el concepto o idea abstracta no incluye todas las operaciones elementales, pero siempre incluye las esenciales. Este hecho raras veces queda documentado cuando se construyen programas, pese a que es fundamental.

  |\/\/\/|
  |      |                /|
  |      |               / /       /                (__)
  | (o)(o)          ____/_/__ __  | |               (@@)       |_|_ 
  C      _)      -=<_________|__|=|<|        /-------\/       (|_|
   | ,___|              \ \       | |       / |     ||        _|_|)
   |   /                 \ \       \       *  ||----||         | |
  /____\                  \|                  ^^    ^^
 /      \
Figura 14

      A veces los programadores novatos se preguntan si es posible definir la abstracción de un objeto sin definir las operaciones de la clase. Si el contexto no es la programación, es fácil encontrar muchos ejemplos en los que no se menciona operación alguna, como se nota en la Figura 14. Si se usa una clase, su abstracción debe mencionar las operaciones públicas más importantes que la caracterizan pero, como el Rep siempre es privado, nunca hay que mencionarlo en la abstracción. Algunos ejemeplos de las operaciones mencionadas en la abstracción de un objeto son estos: "mueve la ficha", "agrega al final del contenedor", "obtiene el [i]-esimo valor", etc. En el caso de funciones, la abstracción puede incluir la mención de la descripción corta de la rutina y los parámetros: "suma", "calcula impuesto", "graba reporte", etc.


Conclusión [<>] [\/] [/\]

      ¿Por qué no me le debo meter al Rep? Para construir programas es necesario seguir un conjunto de buenas prácticas que facilitan la labor, reducen el costo y hacen más fácil el mantenimiento de los módulos. El buen programador usa:

      Cuando uno se le puedo meter al Rep quiebra todas estas buenas prácticas y, como resultado, produce programas de menor calidad y a un mayor costo.


Agradecimientos [<>] [\/] [/\]

      Alejandro Di Mare aportó varias observaciones y sugerencias importantes para mejorar este trabajo.


Código fuente [<>] [\/] [/\]

rational.zip: Todos los fuentes [.zip]
http://www.di-mare.com/adolfo/p/src/rational.zip
Archivo Descripción Texto Doxygen
rational.h Declaraciones y definiciones para la clase "rational"
http://www.di-mare.com/adolfo/p/rational/rational.h
.txt  .html
rational.cpp Implementaciones para la clase "rational"
http://www.di-mare.com/adolfo/p/rational/rational.cpp
.txt  .html
rat-tst.cpp Programa de prueba para la clase "rational"
http://www.di-mare.com/adolfo/p/rational/rat-tst.cpp
.txt  .html
rat-calc.cpp La calculadora polimórfica de Adolfo
http://www.di-mare.com/adolfo/p/rational/rat-calc.cpp
.txt  .html
rat-tst.vcproj Compilación VC++ v7 para rat-tst.cpp
http://www.di-mare.com/adolfo/p/rational/rat-tst.vcproj
.txt .vcproj
rat-calc.vcproj Compilación VC++ v7 para rat-calc.cpp
http://www.di-mare.com/adolfo/p/rational/rat-calc.vcproj
.txt .vcproj
rat-tst.dxg Configuración Doxygen [v 1.3.9.1]
ftp://ftp.stack.nl/pub/users/dimitri/doxygen-1.3.9.1-setup.exe
.txt .dxg
Documentación Doxygen http://www.di-mare.com/adolfo/p/rational/index.html

Bibliografía [<>] [\/] [/\]


           
[DiM-1988] Di Mare, Adolfo: Convenciones de Programación para Pascal, Reporte Técnico ECCI-01-88, Proyecto 326-86-053, Escuela de Ciencias de la Computación e Informática (ECCI), Universidad de Costa Rica (UCR), 1988.
      http://www.di-mare.com/adolfo/p/convpas.htm
[DiM-1991] Di Mare, Adolfo: Tipos Abstractos de Datos y Programación por Objetos, Reporte Técnico PIBDC-03-91, proyecto 326-89-019, Escuela de Ciencias de la Computación e Informática (ECCI), Universidad de Costa Rica (UCR), 1991.
      http://www.di-mare.com/adolfo/p/oop-adt.htm
[DiM-1994] Di Mare, Adolfo: La Implementación de Rational.pas, Reporte Técnico ECCI-94-03, Proyecto 326-89-019, Escuela de Ciencias de la Computación e Informática (ECCI), Universidad de Costa Rica (UCR), 1994.
      http://www.di-mare.com/adolfo/p/rational.htm
[DiM-2008] Di Mare, Adolfo: BUnit.h: Un módulo simple para aprender prueba unitaria de programas en C++, Reporte Técnico ECCI-2008-02, Escuela de Ciencias de la Computación e Informática, Universidad de Costa Rica, 2008.
      http://www.di-mare.com/adolfo/p/BUnit.htm
[Hunt-1998] Hunt, Lance: C# Coding Standards for .NET, 2004.
      http://weblogs.asp.net/lhunt/pages/CSharp-Coding-Standards-document.aspx
[KG-2000] Beck, Kent & Gamma, Erich: JUnit Test Infected: Programmers Love Writing Tests, en More Java gems, Cambridge University Press, New York, NY, USA ISBN 0-521-77477-2 Pages: 357 - 376, 2000.
      http://www.junit.org/junit/doc/testinfected/testing.htm
[KNDK-1997] King, Peter & Naughton, Patrick & DeMoney, Mike & Kanerva, Jonni & Walrath, Kathy & Hommel, Scott: Java Code Conventions, Sun Microsystems, Inc, 1997.
      http://java.sun.com/docs/codeconv/CodeConventions.pdf
[LG-1986] Liskov, Barbara & Guttag, John: Abstraction and Specification in Program Development, MIT Press, 1986.
[SIM-1999] Simonyi, Charles: Hungarian Notation, Microsoft MSDN, 1999.
      http://msdn.microsoft.com/library/default.asp?url=
          /library/en-us/dnvs600/html/hunganotat.asp"
[Str-1998] Stroustrup, Bjarne: The C++ Programming Language, 3rd edition, ISBN 0-201-88954-4; Addison-Wesley, 1998.
      http://www.research.att.com/~bs/3rd.html
[VANH-2004] Van Heesch, Dimitri: Doxygen documentation system for C++, 2004.
      http://www.doxygen.org/index.html

Indice [<>] [\/] [/\]

[-] Resumen
[1] Rep
[2] Invariante
[3] Implementación
[4] Especificación
[5] Abstracción
[-] Conclusión
[-] Agradecimientos
[-] Código fuente

Bibliografía
Indice
Acerca del autor
Acerca de este documento
[/\] Principio [<>] Indice [\/] Final

Acerca del autor [<>] [\/] [/\]

Adolfo Di Mare: Investigador costarricense en la Escuela de Ciencias de la Computación e Informática [ECCI] de la Universidad de Costa Rica [UCR], en donde ostenta el rango de Profesor Catedrático. Trabaja en las tecnologías de Programación e Internet. También es Catedrático de la Universidad Autónoma de Centro América [UACA]. Obtuvo la Licenciatura en la Universidad de Costa Rica, la Maestría en Ciencias en la Universidad de California, Los Angeles [UCLA], y el Doctorado (Ph.D.) en la Universidad Autónoma de Centro América.
Adolfo Di Mare: Costarrican Researcher at the Escuela de Ciencias de la Computación e Informática [ECCI], Universidad de Costa Rica [UCR], where he is full professor and works on Internet and programming technologies. He is Cathedraticum at the Universidad Autónoma de Centro América [UACA]. Obtained the Licenciatura at UCR, and the Master of Science in Computer Science from the University of California, Los Angeles [UCLA], and the Ph.D. at the Universidad Autónoma de Centro América.
[mailto]Adolfo Di Mare <adolfo@di-mare.com>

Acerca de este documento [<>] [\/] [/\]

Referencia: Di Mare, Adolfo: ¡No se le meta al Rep!, Reporte Técnico ECCI-2007-01, Escuela de Ciencias de la Computación e Informática, Universidad de Costa Rica, 2007.
Internet: http://www.di-mare.com/adolfo/p/Rep.htm
Autor: Adolfo Di Mare <adolfo@di-mare.com>
Contacto: Apdo 4249-1000, San José Costa Rica
Tel: (506) 207-4020       Fax: (506) 438-0139
Revisión: ECCI-UCR, Abril 2010
Visitantes:


Copyright © 2007 Adolfo Di Mare
Derechos de autor reservados © 2007 Adolfo Di Mare <adolfo@di-mare.com>
[home] [<>] [/\]